/** 
 *    
 *  Copyright (C) 2008 Marco Speranza <marco.speranza79@gmail.com>
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */
package org.commons.database.jconnectionpool;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.sql.ConnectionEvent;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/**
 * Classe che gestisce il pool di connessioni al DB
 * 
 * @author <a href="mailto:msperanza@users.sourceforge.net">Marco Speranza</a>
 * @version $Id: DataBaseManager.java,v 1.1 2006/08/10 09:48:09 msperanza Exp $
 */
public class PoolConnectionManager {

	

	private static Logger log = Logger.getLogger(PoolConnectionManager.class);
	private final ConnectionPoolDataSource connectionPoolDS;

	private List connPooList;

	volatile private int physicalConnCloseTimeout = 1000;
	volatile private int initialCapacity = 60;
	volatile private int requestedConnection;

	/**
	 * Costruttore
	 * 
	 * @param connectionPoolDataSource
	 *            Instanza del poolConnection data Source.
	 * 
	 */
	public PoolConnectionManager(
			ConnectionPoolDataSource connectionPoolDataSource) {
		connectionPoolDS = connectionPoolDataSource;
	}

	/**
	 * Rilascia una connessione al DB e la rende disponibile
	 * 
	 * @param conn
	 *            Connessione da rilasciare
	 * @throws SQLException
	 *             Errore nella chiusura della connessione al DB
	 */
	final public synchronized void closeConnection(final Connection conn)
			throws SQLException {		
		if (conn != null) {
			log.debug("release connection #" + requestedConnection--);
			conn.close();
		}
	}

	/**
	 * Chiude il pool di connessioni
	 * 
	 * @throws SQLException
	 *             Errore nella chiusura del pool di Connessioni
	 */
	final public synchronized void closePool() throws SQLException {
		int i = 0;

		for (i = 0; i < connPooList.size(); i++) {
			try {
				WrapPooledConnection element = (WrapPooledConnection) connPooList.get(i);
				element.getPooledConnection().close();
			} catch (SQLException e) {
				// ignore
				log.error(e.getMessage(), e);
			}
		}
		connPooList.clear();
		log.debug(" ...done. Closed " + i + "connection polls.");

	}

	/**
	 * Creazione del pool di connessione
	 * 
	 * @param url
	 *            Stringa di connessione al DB
	 * @param user
	 *            Userid utilizzata per connettersi al DB
	 * @param pwd
	 *            Password necessaria per la connessione al DB
	 */
	public void createPool() throws SQLException {
		init();
	}

	/**
	 * Restituisce un nuova connessione al DB
	 * 
	 * @return Nuova connessione al DB
	 * @throws SQLException
	 *             Errore nella creazione della nuova connessione
	 */
	final public synchronized Connection getConnection() throws SQLException {

		log.debug("request new connection #" + ++requestedConnection);
		Connection conn = null;
		PooledConnection connPool = getAvaiableConnection();
		if (connPool == null) {
			log.debug("   creating new poolled connection...");
			connPool = connectionPoolDS.getPooledConnection();
			WrapPooledConnection wrap = new WrapPooledConnection(connPool, this);
			wrap.setAvaiable(false);
			connPool.addConnectionEventListener(wrap);
			addToLast(wrap);
			log.debug("   ... created.");
		}

		conn = connPool.getConnection();
		conn.setAutoCommit(false);		
		return conn;

	}

	public void setInitialPoolCapacity(final int initialCapacity) {
		this.initialCapacity = initialCapacity;
	}

	
	/**
	 * Chuide la connessione fisica.
	 * @param connPool la connection pool da chiudere
	 * @throws SQLException
	 */
	protected synchronized void fairedConnectionClose(final WrapPooledConnection connPool) throws SQLException {
		if (getPoolSize() > initialCapacity) {
			removePoolItem(connPool);
			connPool.getPooledConnection().close();
			log.debug("...closed physical conn - getPoolSize(): " + getPoolSize() + " cap: " + initialCapacity);
		} 
	}
	
	/**
	 * Rende la connessione disponibile e la sposta in prima posizione 
	 * @param connPool la connection pool
	 * @throws SQLException
	 */
	protected synchronized void fairedConnectionAvaiable(final WrapPooledConnection connPool) throws SQLException {
//		if (getPoolSize() > initialCapacity) {
//			removePoolItem(connPool);
//			connPool.getPooledConnection().close();
//		} else {
			moveToFirst(connPool);
//		}
		
	}
	
	protected synchronized void fairedConnectionErrorOccurred(final WrapPooledConnection connPool, final ConnectionEvent event) {
		removePoolItem(connPool);
		if (log.isEnabledFor(Level.ERROR)) {			
			if (event != null && event.getSQLException() != null ) {
				log.error(event.getSQLException().getMessage(), event.getSQLException());
			}
		}
		
		
	}
	
	protected synchronized int getPoolSize() {
		return connPooList.size();
	}
	
	
	private boolean removePoolItem(final Object obj) {
		return connPooList.remove(obj);
	}

	private void addToLast(final Object obj) {
		connPooList.add(obj);
	}

	/**
	 * 
	 * @return PooledConnection torna una pooled connection disponibile oppure
	 *         null
	 */
	private PooledConnection getAvaiableConnection() {
		WrapPooledConnection pConn = null;
		if (connPooList.size() == 0) {
			return null;
		}
		pConn = (WrapPooledConnection) connPooList.get(0);
		if (pConn == null) {
			return null;
		}
		if (pConn.isAvaiable()) {
			log.debug("     return avaialbe connection ");
			pConn.setAvaiable(false);
			moveToLast(pConn);
			return pConn.getPooledConnection();
		}
		return null;
	}

	private void init() throws SQLException {
		connPooList = Collections.synchronizedList(new ArrayList(initialCapacity));
		log.debug("init pool connection...");
		for (int i = 0; i < initialCapacity; i++) {
			try {
				PooledConnection connPool = connectionPoolDS.getPooledConnection();
				WrapPooledConnection wrap = new WrapPooledConnection(connPool, this);
				connPool.addConnectionEventListener(wrap);
				connPooList.add(wrap);
			} catch (SQLException e) {
				log.error("init pool failed! :" + e.getMessage(), e);
				throw e;
			}
		}
		log.debug("...created " + connPooList.size() + " poolledConnection of "+ initialCapacity);
	}


	private void moveToFirst(final Object obj) {
		synchronized (connPooList) {
			Object w = connPooList.remove(connPooList.indexOf(obj));
			connPooList.add(0, w);
		}
	}
	
	private void moveToLast(final Object obj) {
		synchronized (connPooList) {
			Object w = connPooList.remove(connPooList.indexOf(obj));
			connPooList.add(w);
		}
	}

	/**
	 * Torna i ms di deley per la chiusura fisica della connessione
	 * @return the physicalConnCloseTimeout
	 */
	public int getPhysicalConnCloseTimeout() {
		return physicalConnCloseTimeout;
	}

	/**
	 * Setta il valore in ms per il delay della chiusura fisica della connessione
	 * @param physicalConnCloseTimeout the physicalConnCloseTimeout to set
	 */
	public void setPhysicalConnCloseTimeout(int physicalConnCloseTimeout) {
		this.physicalConnCloseTimeout = physicalConnCloseTimeout;
	}

}
